fix(bitbox): guard + self-heal empty wallet address (grey screen)#710
Merged
Conversation
A BitBox wallet could be persisted with an empty on-chain address: the
bitbox_flutter transport coerces a native null into "" (result ?? '') when
the device isn't fully ready right after channel-hash verify, and
createBitboxWallet stored it without validation. On the next launch the
dashboard build read that address through EthereumAddress.fromHex("") which
throws, surfacing in release as a bare grey ErrorWidget. Software wallets are
unaffected because they always derive a real address.
- BitboxService.getEthAddress: single retrying boundary that never returns
empty — a transient empty read self-recovers across attempts, a persistent
one throws BitboxAddressUnavailableException.
- createBitboxWallet / healCurrentBitboxAddress route through it and keep a
format guard before persisting.
- Self-heal: detect a BitBox row with an empty/invalid address at load and
divert to a re-pairing recovery page that re-derives and backfills the
address (local key derivation, no API state), then continues to the
dashboard. Cancel removes the unusable view-wallet so the user is never
stranded.
- Defense-in-depth: a custom ErrorWidget.builder replaces the silent grey box
with a logged, on-brand surface.
This was referenced Jun 9, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Problem
A BitBox (hardware) wallet could be persisted with an empty on-chain address. On the next app launch — after PIN entry — the dashboard build reads that address through
EthereumAddress.fromHex(""), which throws. In release this is uncaught in the build phase and surfaces as a bare grey screen (the defaultErrorWidget). Software wallets are unaffected because they always derive a real address.Root cause
bitbox_flutter'sgetETHAddresscoerces a nativenullinto""at the transport boundary (bitbox_usb_method_channel.dart→return result ?? '';; the iOS/Android handlers return it unvalidated). When the device isn't fully ready (e.g. a transient BLE stall right after channel-hash verify), the address comes back empty, andcreateBitboxWalletpersisted it with no validation — this gap has existed since BitBox support was first added. The pairing ceremony also fetched the address with no device-ready re-check or retry (unlike the existing channel-hash retry loop).What changed
BitboxService.getEthAddressnever returns empty: a transient empty read self-recovers across bounded retries; a persistent one throws the new typedBitboxAddressUnavailableException.createBitboxWallet(and the heal path) route through that boundary and keep a format guard, so an empty/invalid address can never land on disk again. A failed fetch falls back to the pairing flow's existing retry path.ErrorWidget.builderreplaces the silent grey box with a logged, on-brand surface, and routes uncaught build errors throughFlutterError.onError.Test plan
flutter analyzecleanflutter test --exclude-tags golden— full suite green (2334 tests)createBitboxWalletrejects empty/invalid without persisting;getEthAddressretry (first-ok / empty-then-ok viafakeAsync/ persistent-empty throws);currentWalletNeedsAddressRecoverymatrix;healCurrentBitboxAddresshappy + throwHomeBlocdiverts to recovery and clears the flag after a clean loadonCanceldoes not throw on a single-entry stack (+ regression guard)